package com.moyun.syssecurity.handler;

import cn.hutool.core.date.DateUtil;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.moyun.model.common.R;
import com.moyun.model.po.system.SysUserEntity;
import com.moyun.service.system.SysUserService;
import com.moyun.common.task.JobTemplate;
import es.moki.ratelimitj.core.limiter.request.RequestLimitRule;
import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter;
import es.moki.ratelimitj.inmemory.request.InMemorySlidingWindowRequestRateLimiter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Duration;
import java.util.*;

/**
 * <h3>learning_route</h3>
 * <p>自定义登录失败处理器，用于登录失败后的处理</p>
 *
 * @author : ybx
 * @date : 2021-01-24 19:43:59
 **/
@Slf4j
@Component
@RequiredArgsConstructor
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final SysUserService sysUserService;
    private final JobTemplate jobTemplate;
    private final Scheduler scheduler;

    //规则定义：2分钟之内5次机会，就触发限流行为
    Set<RequestLimitRule> rules =
            Collections.singleton(RequestLimitRule.of(Duration.ofMinutes(2), 4));
    // 创建内存限制器limiter
    RequestRateLimiter limiter = new InMemorySlidingWindowRequestRateLimiter(rules);

    HashMap<String, Object> map = new HashMap<>();

    @SneakyThrows
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json; charset=UTF-8");
        PrintWriter out = response.getWriter();
        String errorMsg = getErrorMsg(e);

        // 判断用户是否达到最大错误次数限制
        // TODO: 2022/6/26 没有考虑到用户用手机号登录的场景,暂时去掉
//        isReachedTheLimit(request);

        out.write(JSON.toJSONString(R.error(-1, errorMsg, map.size() == 0 ? null : map)));
        out.flush();
        out.close();
    }

    /**
     * @return boolean
     * @Author yangboxuan
     * @Description 判断用户是否达到限制条件(单位时间内登录错误次数)
     * @Date 2021/9/27 17:37
     * @Param [request]
     **/
    private void isReachedTheLimit(HttpServletRequest request) throws SchedulerException {
        //1. 从request或request.getSession中获取登录用户名
        // TODO 如果此时用户用的是手机号 进行系统登录 呢？
        Map<String, String> params = ServletUtil.getParamMap(request);
        String username = params.get("username");
        //2. 计数器加1，并判断该用户是否已经到了触发了锁定规则
        boolean reachLimit = limiter.overLimitWhenIncremented(username);
        //3. 如果触发了锁定规则(用户已达到最大失误次数)，通过UserDetails告知Spring Security锁定账户
        if (reachLimit) {
            // 1. 判断用户是否已经被锁定,已经锁定的话,不会往下执行
            if (isAlreadyLocked(username)) {
                // 返回还需要多长时间才能解封的信息
                Trigger trigger = scheduler.getTrigger(new TriggerKey(username));
                Date unblockingTime = trigger.getNextFireTime();
                map.put("unBlockTime", DateUtil.formatDateTime(unblockingTime));
            } else {
                // 1. 首先 锁定账户 更新用户表中 该用户的某个字段值
                SysUserEntity user = new SysUserEntity();
                user.setEnable(1);
                this.sysUserService.update(user, new QueryWrapper<SysUserEntity>().eq("user_name", username));
                // 2. 创建该用户解冻倒计时  定时任务
                LinkedHashMap<String, Object> map = new LinkedHashMap<>();
                map.put("username", username);
                // 封装一些信息到定时任务数据中
                JobDataMap jobDataMap = new JobDataMap(map);
                JobDetail jobDetail = JobBuilder.newJob(jobTemplate.getClass()).withIdentity(username, "loginError").setJobData(jobDataMap).build();
                SimpleTrigger simpleTrigger = TriggerBuilder
                        .newTrigger()
                        // 指定触发器组名和触发器名
                        .withIdentity(username)
                        // 指定调度器
                        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                                .withIntervalInMinutes(1))
                        // 1分钟以后再执行
                        .startAt(DateUtil.offsetMinute(new Date(), 3))
                        .build();
                // 将job和trigger添加到scheduler里
                scheduler.scheduleJob(jobDetail, simpleTrigger);
                log.info("用户名为:{#{}} 系统用户,账户已经冻结,冻结时长3min", username);
            }
        }

    }

    /**
     * @return boolean
     * @Author yangboxuan
     * @Description 判断当前用户是否已经被锁定住
     * @Date 2021/9/27 20:10
     * @Param [username]
     **/
    private boolean isAlreadyLocked(String username) throws SchedulerException {
        // 获取所有定时任务的key
        Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.anyJobGroup());
        // 遍历jobKeys,是否包含当前用户名的key
        boolean contains = jobKeys.stream().anyMatch(jobKey -> jobKey.getName().equals(username));
        // 证明当前用户正在锁定中,不用更新用户的锁定状态了.
        return contains;
    }

    // 获取登录异常具体信息
    public String getErrorMsg(Exception exception) {
        String error = "";
        if (exception instanceof UsernameNotFoundException || exception instanceof BadCredentialsException) {
            error = exception.getMessage();
            // 如果是SpringSecurity提示的用户名或者密码错误,则修改为中文提示
            if (error.startsWith("U") || error.startsWith("B")) {
                error = "用户名或密码错误";
            }
        } else if (exception instanceof DisabledException) {
            error = "账户已禁用";
        } else if (exception instanceof LockedException) {
            error = "账户已锁定";
        } else if (exception instanceof AccountExpiredException) {
            error = "账户已过期";
        } else if (exception instanceof CredentialsExpiredException) {
            error = "证书已过期";
        } else {
            // 不是系统定义异常，是我们自定义的异常. 例如我们自定义的验证码校验失败的异常
            error = exception.getMessage();
        }
        return error;
    }
}
